# jsb/runner.py
#
#

""" threads management to run jobs. """

## jsb imports

from jsb.lib.threads import getname, start_new_thread, start_bot_command
from jsb.utils.exception import handle_exception
from jsb.utils.locking import locked, lockdec
from jsb.utils.lockmanager import rlockmanager, lockmanager
from jsb.utils.generic import waitevents
from jsb.utils.trace import callstack, whichmodule
from jsb.lib.threadloop import RunnerLoop
from jsb.lib.callbacks import callbacks

## basic imports

import Queue
import time
import thread
import random
import logging
import sys

## Runner class

class Runner(RunnerLoop):

    """
        a runner is a thread with a queue on which jobs can be pushed. 
        jobs scheduled should not take too long since only one job can 
        be executed in a Runner at the same time.

    """

    def __init__(self, name="runner", doready=True):
        RunnerLoop.__init__(self, name)
        self.working = False
        self.starttime = time.time()
        self.elapsed = self.starttime
        self.finished = time.time()
        self.doready = doready
        self.nowrunning = ""
        self.longrunning = []

    def handle(self, descr, func, *args, **kwargs):
        """ schedule a job. """
        self.working = True
        try:
            #rlockmanager.acquire(getname(str(func)))
            logging.debug('running %s: %s' % (descr, self.nowrunning))
            self.starttime = time.time()
            func(*args, **kwargs)
            self.finished = time.time()
            self.elapsed = self.finished - self.starttime
            if self.elapsed > 5:
                logging.debug('ALERT %s %s job taking too long: %s seconds' % (descr, str(func), self.elapsed))
        except Exception, ex: handle_exception()
        #finally: rlockmanager.release()
        self.working = False

    def done(self, event):
        try: int(event.cbtype)
        except ValueError:
            if event.cbtype not in ['PING', 'NOTICE', 'TICK60']: logging.warn("===> %s" % event.cbtype)
                            
## BotEventRunner class

class BotEventRunner(Runner):

    def handle(self, speed, args):
        """ schedule a bot command. """
        try:
            descr, func, bot, ievent = args
            self.starttime = time.time()
            #lockmanager.acquire(getname(str(func)))
            logging.info("event handler is %s" % str(func))
            if self.nowrunning in self.longrunning: 
                logging.warn("putting %s on longrunner" % self.name)
                longrunner.put(ievent.speed or speed, descr, func, bot, ievent)
                return
            self.working = True
            func(bot, ievent)
            time.sleep(0.001)
            #self.done(ievent)
            self.finished = time.time()
            self.elapsed = self.finished - self.starttime
            if self.elapsed > 5:
                if self.nowrunning not in self.longrunning: self.longrunning.append(self.nowrunning)
                logging.info('ALERT %s %s job taking too long: %s seconds' % (descr, str(func), self.elapsed))
            if not ievent.type == "OUTPUT" and not ievent.dontclose: ievent.ready()
        except Exception, ex:
            handle_exception()
        #finally: lockmanager.release(getname(str(func)))
        self.working = False
        logging.info("finished  %s" % self.nowrunning)

class LongRunner(Runner):

    def handle(self, speed, args):
        """ schedule a bot command. """
        try:
            descr, func, bot, ievent = args
            self.starttime = time.time()
            #lockmanager.acquire(getname(str(func)))
            self.nowrunning = getname(func)
            logging.info("long event handler is %s" % str(func))
            self.working = True
            func(bot, ievent)
            time.sleep(0.001)
            #self.done(ievent)
            if not ievent.type == "OUTPUT" and not ievent.dontclose: ievent.ready()
        except Exception, ex:
            handle_exception()
        #finally: lockmanager.release(getname(str(func)))
        self.working = False
        logging.info("long finished - %s" % self.nowrunning)

## Runners class

class Runners(object):

    """ runners is a collection of runner objects. """

    def __init__(self, max=100, runnertype=Runner, doready=True):
        self.max = max
        self.runners = []
        self.runnertype = runnertype
        self.doready = doready

    def names(self):
        res = []
        for runner in self.runners: res.append(runner.name)
        return res

    def size(self): return len(self.runners)

    def runnersizes(self):
        """ return sizes of runner objects. """
        result = []
        for runner in self.runners: result.append("%s - %s" % (len(runner.queue), runner.name))
        return result

    def stop(self):
        """ stop runners. """
        for runner in self.runners: runner.stop()

    def start(self):
        """ overload this if needed. """
        pass
 
    def put(self, speed, *data):
        """ put a job on a free runner. """
        logging.debug("size is %s" % len(self.runners))
        for runner in self.runners:
            if not len(runner.queue):
                runner.put(speed, *data)
                return
        runner = self.makenew()
        runner.put(speed, *data)
         
    def running(self):
        """ return list of running jobs. """
        result = []
        for runner in self.runners:
            if runner.working: result.append(runner.nowrunning)
        return result

    def makenew(self):
        """ create a new runner. """
        runner = None
        for i in self.runners:
            if not len(i.queue): i.stop()
        if len(self.runners) < self.max:
            runner = self.runnertype()
            runner.start()
            self.runners.append(runner)
        else: runner = random.choice(self.runners)
        return runner

    def cleanup(self):
        """ clean up idle runners. """
        if not len(self.runners): logging.debug("nothing to clean")
        r = []
        for index in range(len(self.runners)-1, -1, -1):
            runner = self.runners[index]
            logging.debug("cleanup %s" % runner.name)
            if not len(runner.queue):
                try: runner.stop() ; del self.runners[index] ; r.append("%s-%s" % (runner.name, index))
                except IndexError: pass
                except: handle_exception()
        logging.info("cleaned %s" %  r)
        logging.info("now running: %s" % self.running())
        
## show runner status

def runner_status():
    print cmndrunner.runnersizes()
    print callbackrunner.runnersizes()


## global runners

cmndrunner = defaultrunner = Runners(50, BotEventRunner) 
longrunner = Runners(80, LongRunner)
callbackrunner = Runners(80, BotEventRunner)
waitrunner = Runners(10, BotEventRunner)

## cleanup 

def runnercleanup(bot, event):
    cmndrunner.cleanup()
    logging.debug("cmndrunner sizes: %s" % str(cmndrunner.runnersizes()))
    longrunner.cleanup()
    logging.debug("longrunner sizes: %s" % str(longrunner.runnersizes()))
    callbackrunner.cleanup()
    logging.debug("callbackrunner sizes: %s" % str(cmndrunner.runnersizes()))
    waitrunner.cleanup()
    logging.debug("waitrunner sizes: %s" % str(cmndrunner.runnersizes()))

callbacks.add("TICK60", runnercleanup)

def size():
    return "cmnd: %s - callbacks: %s - wait: %s - longrunning: %s" % (cmndrunner.size(), callbackrunner.size(), waitrunner.size(), longrunner.size())
